/*
* ALMA - Atacama Large Millimiter Array
* (c) Universidad Tecnica Federico Santa Maria, 2008
* Copyright by ESO (in the framework of the ALMA collaboration),
* All rights reserved
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307 USA
*/
package alma.acs.monitoring;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.MalformedURLException;
import javax.management.JMX;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
/**
* Class used for connecting with a {@link RemoteThreads} MBean implementation.
* The connection can be done with a local process, as well as with a process
* running in a remote machine, with the RMI connector enabled on it.
* <p>
* For further details on the activation of the RMI connector, you can see
* <a href="http://java.sun.com/docs/books/tutorial/jmx/remote/jconsole.html">
* http://java.sun.com/docs/books/tutorial/jmx/remote/jconsole.html</a>
*
* @author rtobar
* @since ACS 7.0
*/
public class RemoteThreadsClient {
/**
* Default port used by the RMI client-side connector
*/
public static final int RMI_DEFAULT_PORT = 9999;
/**
* Property name for getting a local JMX connector
*/
private static final String LOCAL_CONNECTOR_ADDRESS =
"com.sun.management.jmxremote.localConnectorAddress";
private JMXServiceURL remoteURL = null;
private MBeanServerConnection remote = null;
private RemoteThreadsMBean rtmb = null;
private JMXConnector connector = null;
/**
* Gets a new <code>RemoteThreadsClient</code> ready to be connected to the
* remote JVM indicated by the name of the launcher class (the one containing the
* <code>public static void main(String[])</code> method).
* @param className The name of the launcher class of the remote process
* @throws RemoteThreadsException Is thrown in the following cases:
* <ul>
* <li> The given PID isn't present in the list of java processes, or
* it can't be retreived by the <code>jps</code> program.</li>
* <li> Cannot connect to the remote JVM or can't attach to it.</li>
* <li> Can't load the JMX agent on the remote JVM or can't initialize it
* (if it isn't already loaded).</li>
* </ul>
*/
public RemoteThreadsClient(String className) throws RemoteThreadsException {
int pid = 0;
try {
pid = getRemotePID(className);
} catch (IOException e) {
throw new RemoteThreadsException("Error while getting remote PID!",e);
}
if( pid == 0 ) {
// We haven't found the PID :(.
throw new RemoteThreadsException("Can't find PID for the " + className + " class");
} else {
// We have found the PID :)
try {
getServiceURL(pid);
} catch (IOException e) {
throw new RemoteThreadsException("Can't connect to remote VM",e);
} catch (AgentLoadException e) {
throw new RemoteThreadsException("Can't load the JMX agent into the remote JVM",e);
} catch (AttachNotSupportedException e) {
throw new RemoteThreadsException("Can't attach to the remote JVM",e);
} catch (AgentInitializationException e) {
throw new RemoteThreadsException("Can't initialize the remote JMX agent",e);
}
}
}
/**
* Gets a new <code>RemoteThreadsClient</code> ready to be connected to the
* remote JVM indicated by the given PID
* @param pid The PID of the remote java process
* @throws RemoteThreadsException If:
* <ul>
* <li> PID is less or equal to 0.</li>
* <li> Cannot connect to the remote JVM or can't attach to it.</li>
* <li> Can't load the JMX agent on the remote JVM or can't initialize it
* (if it isn't already loaded).</li>
* </ul>
*/
public RemoteThreadsClient(int pid) throws RemoteThreadsException {
if( pid <= 0 ) {
throw new RemoteThreadsException("PID must be grater than zero");
}
try {
getServiceURL(pid);
} catch (IOException e) {
throw new RemoteThreadsException("Can't connect to remote VM",e);
} catch (AgentLoadException e) {
throw new RemoteThreadsException("Can't load the JMX agent into the remote JVM",e);
} catch (AttachNotSupportedException e) {
throw new RemoteThreadsException("Can't attach to the remote JVM",e);
} catch (AgentInitializationException e) {
throw new RemoteThreadsException("Can't initialize the remote JMX agent",e);
}
}
/**
* Gets a new <code>RemoteThreadsClient</code> ready to be connected to the
* remote JVM present in the given host address. It uses the {@link #RMI_DEFAULT_PORT}
* for connecting to the remote host.
* @param remoteHost The remote host where the remote JVM is placed
* @throws RemoteThreadsException If a malformed URL is produced with the given
* host address.
*/
public RemoteThreadsClient(InetAddress remoteHost) throws RemoteThreadsException {
try {
getServiceURL(remoteHost, RMI_DEFAULT_PORT);
} catch (MalformedURLException e) {
throw new RemoteThreadsException(e);
}
}
/**
* Gets a new <code>RemoteThreadsClient</code> ready to be connected to the
* remote JVM present in the given host address and port.
* @param remoteHost The remote host where the remote JVM is placed
* @param remotePort The port to be used for the RMI connection
* @throws RemoteThreadsException If a malformed URL is produced with the given
* host address and port.
*/
public RemoteThreadsClient(InetAddress remoteHost, int remotePort) throws RemoteThreadsException {
if( remotePort <= 0 ) {
throw new RemoteThreadsException("Port must be grater than zero");
}
try {
getServiceURL(remoteHost,remotePort);
} catch (MalformedURLException e) {
throw new RemoteThreadsException(e);
}
}
/**
* Connects the object instance to the remote JVM agent and gets a direct connection
* with the MBeanServer in order to be able to get the remote MBean.
*
* @return If the connection has been sucessful or not
*/
public boolean connect() {
try {
connector = JMXConnectorFactory.connect(remoteURL);
} catch (IOException e) {
System.err.println("Can't connect to the remote URL");
return false;
}
try {
remote= connector.getMBeanServerConnection();
} catch (IOException e) {
System.err.println("Can't get a connection to the remote MBeanServer");
return false;
}
return true;
}
/**
* Closes the connection with the remote MBeanServer.
* @throws RemoteThreadsException If the connection has not been opened.
*/
public void close() throws RemoteThreadsException {
if( connector != null) {
try {
connector.close();
} catch (IOException e) {
System.err.println("Cannot close connection to the remote MBeanServer");
}
} else {
throw new RemoteThreadsException("The connection has not been opened");
}
}
/**
* Returns an object representing the MBean registered on the remote JVM agent. If the
* MBean has not been registered, then it is registered and then retreived.
* @return A {@link RemoteThreadsMBean} object representing the MBean registered
* on the remote JVM agent.
* @throws RemoteThreadsException If:
* <ul>
* <li>The MBean can't be registered on the remote JVM agent</li>
* <li>A local reference for this MBean can't be get</li>
* </ul>
*/
public RemoteThreadsMBean getMBean() throws RemoteThreadsException {
if ( rtmb == null ) {
try {
ObjectName remoteThreadsName = new ObjectName("alma.acs.monitoring:type=RemoteThreads");
if( ! remote.isRegistered(remoteThreadsName))
remote.createMBean("alma.acs.monitoring.RemoteThreads", remoteThreadsName);
rtmb = JMX.newMBeanProxy(remote,remoteThreadsName,RemoteThreadsMBean.class);
} catch(MalformedObjectNameException e) {
// shouldn't get never get here
} catch(MBeanRegistrationException e) {
throw new RemoteThreadsException("Can't register the MBean in the remote server",e);
} catch(Exception e) {
throw new RemoteThreadsException("Can't get a reference to the remote MBean",e);
}
}
return rtmb;
}
private int getRemotePID(String className) throws IOException {
String s;
Integer remotePID = null;
// The only way to get the java processes list is to use the
// jps command (it's like a ps, but for only java processes).
// The command output is parsed to get the name of the runnable
// class. Then, the associated PID is stored.
//
// The sun.tools.jps.Jps class in the tools.jar jarfile is used
// by the jps process. Anyways, this class only has a main
// method, so no information can be retrieved by means of
// public methods. So, if the main method is invoked in a static
// way, the output should be parsed as well as it's being done
// now (this is far more complex, since the stdout should be
// redirected to another PrinterStream, and then read from it).
// Better we execute the jps command and read directly from its
// associated InputStream.
Process p = Runtime.getRuntime().exec("jps -lm");
BufferedReader stdInput = new BufferedReader(new
InputStreamReader(p.getInputStream()));
// read the output from the command
while ((s = stdInput.readLine()) != null) {
String[] ss = s.split("[\t ]+");
if (ss.length > 1) {
if (ss[1].equals(className)) {
remotePID = Integer.valueOf(ss[0]);
break;
}
}
}
stdInput.close();
if( remotePID == null ){
//System.err.println("Can't obtain PID for the given class");
return 0;
}
return remotePID.intValue();
}
private void getServiceURL(int pid)
throws IOException, AgentLoadException, RemoteThreadsException,
AttachNotSupportedException, AgentInitializationException {
// attach to the target application
final VirtualMachine vm = VirtualMachine.attach(String.valueOf(pid));
// get the connector address
String connectorAddress =
vm.getAgentProperties().getProperty(LOCAL_CONNECTOR_ADDRESS);
// no connector address, so we start the JMX agent
if (connectorAddress == null) {
String agent = vm.getSystemProperties().getProperty("java.home") +
File.separator + "lib" + File.separator + "management-agent.jar";
vm.loadAgent(agent);
// agent is started, get the connector address
connectorAddress =
vm.getAgentProperties().getProperty(LOCAL_CONNECTOR_ADDRESS);
if ( connectorAddress == null )
throw new RemoteThreadsException("Can't get the remote JVM connector address");
}
remoteURL = new JMXServiceURL(connectorAddress);
}
private void getServiceURL(InetAddress host, int port) throws MalformedURLException {
String hostName = host.getHostName();
String connectorAddress = "service:jmx:rmi:///jndi/rmi://" +
hostName + ":" +
port + "/jmxrmi";
remoteURL = new JMXServiceURL(connectorAddress);
}
}